<!--
 * @Author: 蔡继周
 * @Date: 2021-11-22 17:46:05
 * @LastEditTime: 2021-11-23 10:33:12
 * @LastEditors: Please set LastEditors
 * @Description: 文章目录生成组件
-->
<template>
  <div class="side-catalog" :style="{ height }">
    <div v-if="title" class="side-catalog__title">
      {{ title }}
    </div>
    <div class="side-catalog__list">
      <div>
        <div
          v-for="item in topList"
          :key="item.ref"
          :style="[
            { 'padding-left': iconLeft ? '0px' : getTitleMargin(item.level) },
            active === item.ref ? { color: activeColor } : '',
          ]"
          class="side-catalog__list-item"
          @click="activeAnchor(item.ref)"
          :class="{
            'side-catalog__list-item--active': active === item.ref,
            'side-catalog__list-item--child': isChildren(item.level),
          }"
        >
          <!-- 每行插槽 -->
          <slot
            name="row"
            v-bind:level="item.level"
            v-bind:isActive="active === item.ref"
            v-bind:title="item.title"
          >
            <!-- 每行icon插槽 -->
            <slot
              name="default"
              v-bind:level="item.level"
              v-bind:isActive="active === item.ref"
            >
              <i
                class="side-catalog__list-item-icon"
                :class="{
                  'side-catalog__list-item-icon--child': isChildren(item.level),
                }"
                :style="active === item.ref ? { color: activeColor } : ''"
              />
            </slot>
            <span
              class="side-catalog__list-item-title"
              :class="[
                `side-catalog__list-item-title--level${item.level || 1}`,
              ]"
              :title="item.title"
              :style="[
                active === item.ref ? { color: activeColor } : '',
                { 'padding-left': iconLeft ? getTitleMargin(item.level) : '' },
              ]"
              >{{ item.title }}</span
            >
          </slot>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import debounce from "lodash.debounce";
import throttle from "lodash.throttle";
import {
  defineComponent,
  reactive,
  ref,
  computed,
  onMounted,
  nextTick,
  toRefs,
} from "vue";
export default defineComponent({
  name: "SideCatalog",
  emits: ["title-click"],
  props: {
    // 是否开启dom监听,dom有变化主动更新各个ref的offsetTop值
    watch: {
      type: Boolean,
      default: false,
    },
    container: {
      type: String,
      required: false,
    },
    levelList: {
      type: Array,
      default() {
        return ["h2", "h3", "h4", "h5"];
      },
    },
    activeColor: {
      type: String,
      default: "#006bff",
    },
    height: {
      type: String,
      default: "",
    },
    levelGap: {
      type: Number,
      default: 20,
    },
    iconLeft: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "",
    },
  },

  setup(props) {
    const active = ref("");
    const isBeforeDestroy = ref(false);
    const itemClicking = ref(false);
    const state = reactive({
      refTopMap: {},
      topList: [],
      reverseTopList: [],
      observer: null,
      debounceIntoView: null,
      throttleScroll: null,
    });
    const scrollElement = computed({
      get: () => {
        return document.querySelector(props.container);
      },
      set: (val) => {
        document.querySelector(props.container).scrollTop = val;
      },
    });
    onMounted(() => {
      state.debounceIntoView = debounce(activeIntoView, 250);
      state.throttleScroll = throttle(scrollHandle, 200);
      setOffsetParent();
      setTopList();
      initActive();
      scrollElement.value.addEventListener("scroll", state.throttleScroll);
      if (!props.watch) return;
      // 等待dom渲染完成之后监听
      setTimeout(() => {
        setWatcher();
      }, 500);
    });
    const activeIntoView = (edge) => {
      // 等active元素改变后
      nextTick(() => {
        const activeEl = document.querySelector(
          ".side-catalog__list-item--active"
        );
        if (!activeEl) return;
        // 顶部或者底部 scrollIntoView为smooth时无效
        activeEl.scrollIntoView({
          block: "center",
          behavior: edge ? "auto" : "smooth",
        });
      });
    };
    // 点击title
    const activeAnchor = (ref) => {
      if (active.value === ref) return;
      // 点击title 会触发scroll事件,在内容高度不够的情况下点击的title和active的title会有出入
      // 所以点击的时候先return掉scroll事件
      itemClicking.value = true;
      scrollElement.value = state.refTopMap[ref];
      active.value = ref;
      state.debounceIntoView();
      // 等待页面滚动完成
      setTimeout(() => {
        itemClicking.value = false;
      }, 150);
      emit("title-click", ref);
    };
    // 获取offsetTop数组
    const setTopList = () => {
      if (isBeforeDestroy.value) return;
      state.topList = [];
      let headlevel = {};
      props.levelList.forEach((item, index) => {
        headlevel[item] = index + 1;
      });
      const childrenList = Array.from(
        document.querySelectorAll(`${props.container}>*`)
      );
      childrenList.forEach((item, index) => {
        const nodeName = item.nodeName.toLowerCase();
        if (props.levelList.includes(nodeName)) {
          state.topList.push({
            ref: `${item.nodeName}-${index}`,
            title: item.innerText,
            offsetTop: item.offsetTop,
            level: headlevel[nodeName],
          });
          state.refTopMap[`${item.nodeName}-${index}`] = item.offsetTop;
        }
      });
      state.reverseTopList = JSON.parse(
        JSON.stringify(state.topList)
      ).reverse();
      console.log(state.topList);
      console.log(state.reverseTopList);
    };
    // scroll事件
    const scrollHandle = () => {
      // 点击title的滚动不触发
      if (itemClicking.value) return;
      const { scrollTop, clientHeight, scrollHeight } = scrollElement.value;
      // 到达顶部
      if (scrollTop === 0) {
        initActive();
        return;
      }
      // 到达底部
      if (scrollTop + clientHeight >= scrollHeight) {
        initActive(true);
        return;
      }
      state.reverseTopList.some((item) => {
        if (scrollTop >= item.offsetTop) {
          active.value = item.ref;
          state.debounceIntoView();
          return true;
        }
        return false;
      });
    };
    const initActive = (last) => {
      if (!state.topList.length) return;
      const index = last ? state.topList.length - 1 : 0;
      active.value = state.topList[index].ref;
      state.debounceIntoView(true);
    };
    const isChildren = (level) => {
      return level && level > 1;
    };
    const getTitleMargin = (level) => {
      return level
        ? `${parseInt(level, 10) * props.levelGap}px`
        : props.levelGap + "px";
    };
    // 需要为scrollElement设置相对定位(offsetParent)
    // offsetParent(定位元素或者最近的 table,td,th,body)
    const setOffsetParent = () => {
      const ele = document.querySelector(props.container);
      if (ele.style.position) return;
      ele.style.position = "relative";
    };

    const setWatcher = () => {
      // 设置dom监听
      state.observer = new MutationObserver(debounce(setTopList, 300));
      state.observer.observe(document.querySelector(props.container), {
        childList: true,
        subtree: true,
        attributes: true,
      });
    };
    return {
      active,
      isBeforeDestroy,
      itemClicking,
      state,
      scrollElement,
      scrollElement,
      activeAnchor,
      getTitleMargin,
      isChildren,
      ...toRefs(state),
    };
  },
  beforeDestroy() {
    if (!this.watch) return;
    // beforeDestroy时,解绑dom监听之前,会触发observer监听的setTopList函数
    // 导致报错,需要用变量控制
    this.isBeforeDestroy = true;
    // 解绑dom监听
    this.observer.disconnect();
    this.scrollElement.removeEventListener("scroll", this.throttleScroll);
  },
});
</script>
<style lang="scss" src="./main.scss"></style>